iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

圖書管理系統完成書籍的 CRUD 之後,接下來要面對的就是使用者。

要做借還書功能,系統必須先知道「是誰借了這本書」。因此,需要一套會員機制。今天先把「會員註冊」建立起來。

為什麼密碼絕對不能存明文?

如果資料庫直接存明文,會有以下問題:

  1. 資料庫外洩直接裸奔:不管是遇到 SQL Injection 攻擊、伺服器被侵入,還是備份檔外流,駭客拿到資料庫的瞬間,就能直接看清所有人的帳號密碼。
  2. 加密(Encryption)與雜湊(Hash)別搞混:
    • 加密是雙向的,有金鑰就能解密還原成原始密碼。
    • 雜湊是單向的,這是一種不可逆的數學轉換。我們不需要知道使用者的原始密碼是什麼,只需要把輸入的字串再次做 Hash,比對算出來的亂碼跟資料庫裡的亂碼是不是同一串即可。

步驟一:讓 Claude Code 幫忙產生註冊功能

觀念釐清後,在專案目錄下啟動 Claude Code,下達以下 Prompt:

請幫我寫一個 register.php。前端要包含帳號與密碼表單,後端接收 POST 請求後,要檢查欄位是否為空、確認帳號有沒有重複。最重要的是,密碼必須使用 password_hash() 處理後,才能寫入 members 資料表。

生成的register.php

    } elseif ($password !== $passwordConfirm) {
        $errors[] = '兩次輸入的密碼不一致'; // 兩次密碼輸入不同
    }

    // ------------------------------------------------------------
    // 子區塊:檢查帳號是否已被註冊(重複帳號)
    // ------------------------------------------------------------
    if (empty($errors)) { // 前面欄位驗證都通過才查資料庫,避免無意義的查詢
        $checkSql = 'SELECT id FROM members WHERE email = :email'; // 依 email 查詢是否已存在
        $checkStmt = $pdo->prepare($checkSql); // 準備查詢語句
        $checkStmt->execute([':email' => $email]); // 綁定帳號並執行查詢

        if ($checkStmt->fetch() !== false) { // 查到資料代表帳號已存在
            $errors[] = '這個帳號已經被註冊,請改用其他 email'; // 加入重複帳號錯誤訊息
        }
    }
    // ------------------------------------------------------------
    // 子區塊:驗證通過後,雜湊密碼並寫入 members 資料表
    // ------------------------------------------------------------
    if (empty($errors)) {
        $passwordHash = password_hash($password, PASSWORD_DEFAULT); // 用 password_hash() 產生安全的密碼雜湊,絕不儲存明文密碼
        try {
            $insertSql = 'INSERT INTO members (name, email, password) VALUES (:name, :email, :password)'; // role 使用資料表預設值 member
            $insertStmt = $pdo->prepare($insertSql); // 準備新增語句
            $insertStmt->execute([
                ':name' => $name, // 綁定姓名
                ':email' => $email, // 綁定帳號
                ':password' => $passwordHash, // 綁定雜湊後的密碼
            ]);
            header('Location: index.php'); // 註冊成功導回首頁
            exit; // 導向後立刻結束,避免下方 HTML 繼續輸出
        } catch (PDOException $e) {
            if ($e->getCode() === '23000') { // 23000 為唯一鍵衝突,理論上前面已檢查過,這裡是防止併發情境下的極端情況
                $errors[] = '這個帳號已經被註冊,請改用其他 email';
            } else {
                $errors[] = '註冊失敗,請稍後再試'; // 其他資料庫錯誤,不把細節暴露給使用者
            }
        }
    }
}

?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<title>會員註冊 - 圖書管理系統</title>
<style>
    body {
        font-family: "Microsoft JhengHei", Arial, sans-serif;
        background-color: #f5f5f5;
        color: #333;
        margin: 0;
        padding: 2rem;
    }
    h1 {
        text-align: center;
        margin-bottom: 1.5rem;
    }
    form {
        width: 100%;
        max-width: 480px;
        margin: 0 auto;
        background-color: #fff;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        padding: 1.5rem 2rem;
        box-sizing: border-box;
    }
    .field {
        margin-bottom: 1rem;
    }
    label {
        display: block;
        margin-bottom: 0.35rem;
        font-weight: bold;
    }
    input[type="text"],
    input[type="email"],
    input[type="password"] {
        width: 100%;
        padding: 0.5rem;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        font-size: 1rem;
    }
    button {
        width: 100%;
        padding: 0.75rem;
        background-color: #2c3e50;
        color: #fff;
        border: none;
        border-radius: 4px;
        font-size: 1rem;
        cursor: pointer;
    }
    button:hover {
        background-color: #1a252f;
    }
    .errors {
        max-width: 480px;
        margin: 0 auto 1rem;
        background-color: #fdecea;
        color: #b71c1c;
        border: 1px solid #f5c6cb;
        border-radius: 4px;
        padding: 0.75rem 1rem;
    }
    .errors ul {
        margin: 0;
        padding-left: 1.25rem;
    }
    .back-link {
        display: block;
        max-width: 480px;
        margin: 1rem auto 0;
        text-align: center;
    }
</style>
</head>
<body>
    <h1>會員註冊</h1>

<?php // 區塊:顯示驗證或寫入錯誤訊息 ?>
<?php if (!empty($errors)): ?>
    <div class="errors">
        <ul>
<?php foreach ($errors as $error): ?>
            <li><?= htmlspecialchars($error) ?></li>
<?php endforeach; ?>
        </ul>
    </div>
<?php endif; ?>

    <!-- 區塊:註冊表單,欄位包含姓名、帳號(email)、密碼、確認密碼 -->
    <form method="POST" action="register.php">
        <div class="field">
            <label for="name">姓名</label>
            <input type="text" id="name" name="name" value="<?= htmlspecialchars($name) ?>" required>
        </div>
        <div class="field">
            <label for="email">帳號 (email)</label>
            <input type="email" id="email" name="email" value="<?= htmlspecialchars($email) ?>" required>
        </div>
        <div class="field">
            <label for="password">密碼</label>
            <input type="password" id="password" name="password" required>
        </div>
        <div class="field">
            <label for="password_confirm">確認密碼</label>
            <input type="password" id="password_confirm" name="password_confirm" required>
        </div>
        <button type="submit">註冊</button>
    </form>

    <a class="back-link" href="index.php">返回書籍列表</a>
</body>
</html>

步驟二:執行與瀏覽器驗證

  1. 正常測試帳號:驗證正常註冊流程,成功後應自動導回 index.php,且資料庫的 password 欄位會是一串雜湊碼。
    正常測試帳號
  2. 重複帳號測試(測試重複 Email 阻擋):測試系統是否會正確跳出 「這個帳號已經被註冊,請改用其他 email」 的警告訊息。
    重複帳號測試
  3. 密碼不一致測試(測試前端/後端比對):試系統是否會攔截並顯示 「兩次輸入的密碼不一致」。
    密碼不一致
  4. XSS 特殊字元測試(測試 HTML 轉義安全性):輸入含有 HTML/JS 標籤的內容(如 ``)。系統無跳出彈窗,並安全轉義寫入資料庫,證明 PDO Prepared Statements 與 htmlspecialchars() 發揮作用,成功抵禦 SQLi 與 XSS 攻擊。
    特殊字元

今天完成了會員註冊功能,並驗證了雜湊處理與資安防護。明天將實作「會員登入與 Session 狀態管理」。


上一篇
Day 14 PHP 修改與刪除書籍功能
下一篇
Day 16 會員登入實作、password_verify 與 Session 狀態管理
系列文
從零打造圖書管理系統:WSL2 × MySQL × Claude Code 的整合實作 共 17 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言